// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CloudDriveManager.cs" company="AzureContrib (Open source library http://azurecontrib.codeplex.com)">
//   Microsoft Public License (Ms-PL)
// </copyright>
// <summary>
//   An implementation of <see cref="ICloudDriveManager" /> that handles cloud drives.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

using AzureContrib.WindowsAzure.Diagnostics;
using AzureContrib.WindowsAzure.ServiceRuntime;
using AzureContrib.WindowsAzure.StorageClient.Properties;

using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

namespace AzureContrib.WindowsAzure.StorageClient
{
	/// <summary>
	/// An implementation of <see cref="ICloudDriveManager"/> that handles cloud drives.
	/// </summary>
	public class CloudDriveManager : ICloudDriveManager
	{
		/// <summary>
		/// By convention this value is used for a local cache for cloud drives.
		/// </summary>
		public const string DefaultLocalDriveCacheName = "AzureContrib.WindowsAzure.ServiceRuntime.CloudDriveManager.DefaultLocalDriveCacheName";

		private readonly ITrace trace;

		private readonly ILocalResourceManager localResourceManager;
		private readonly string localResourceCacheName;

		private readonly IDictionary<DirectoryInfo, DriveAndCredentials> managedDrives;
		private bool cacheInitialized;

		/// <summary>
		/// Initializes a new instance of the <see cref="CloudDriveManager"/> class. 
		/// </summary>
		/// <param name="localResourceManager">
		/// A local resource manager
		/// </param>
		/// <remarks><see cref="DefaultLocalDriveCacheName"/> is used.</remarks>
		public CloudDriveManager(ILocalResourceManager localResourceManager)
			: this(localResourceManager, DefaultLocalDriveCacheName, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CloudDriveManager"/> class. 
		/// </summary>
		/// <param name="localResourceManager">
		/// A local resource manager
		/// </param>
		/// <param name="trace">The current instance of <see cref="ITrace"/> to trace to.</param>
		/// <remarks><see cref="DefaultLocalDriveCacheName"/> is used.</remarks>
		public CloudDriveManager(ILocalResourceManager localResourceManager, ITrace trace)
			: this(localResourceManager, DefaultLocalDriveCacheName, trace)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CloudDriveManager"/> class. 
		/// </summary>
		/// <param name="localResourceManager">
		/// A local resource manager
		/// </param>
		/// <param name="localResourceCacheName">
		/// A name for the local chache with the convention <see cref="DefaultLocalDriveCacheName"/>.
		/// </param>
		public CloudDriveManager(ILocalResourceManager localResourceManager, string localResourceCacheName)
			: this(localResourceManager, localResourceCacheName, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CloudDriveManager"/> class. 
		/// </summary>
		/// <param name="localResourceManager">
		/// A local resource manager
		/// </param>
		/// <param name="localResourceCacheName">
		/// A name for the local chache with the convention <see cref="DefaultLocalDriveCacheName"/>.
		/// </param>
		/// <param name="trace">The current instance of <see cref="ITrace"/> to trace to.</param>
		public CloudDriveManager(ILocalResourceManager localResourceManager, string localResourceCacheName, ITrace trace)
		{
			this.localResourceManager = localResourceManager;
			this.localResourceCacheName = localResourceCacheName;
			this.trace = trace ?? new Trace(typeof(CloudDriveManager), new VerboseCatchallTraceSwitchProvider());

			if (localResourceCacheName == DefaultLocalDriveCacheName)
			{
				this.trace.Information(Strings.LocalDriveCacheNameConventionUsed, DefaultLocalDriveCacheName);
			}

			managedDrives = new Dictionary<DirectoryInfo, DriveAndCredentials>();
		}

		/// <summary>
		/// Gets the total cache size allocated in this configuration. <seealso cref="ICloudDriveManager.TotalCacheInMegabytes" />
		/// </summary>
		public int TotalCacheInMegabytes { get; private set; }

		/// <summary>
		/// Gets the total cache size remaining in this configuration. <seealso cref="ICloudDriveManager.RemainingCache" />
		/// </summary>
		public int RemainingCache { get; private set; }

		/// <summary>
		/// Gets data on all the drives mounted to this instance.
		/// </summary>
		/// <returns>An enumeration of windows root path, cloud drive page blob source uri and storage credentials for each mounted drive.</returns>
		public IEnumerable<PageBlobDriveInfo> MountedDrives
		{
			get
			{
				WarmUpCache();

				return CloudDrive.GetMountedDrives().Select(CreateGetEnumeratorItem);
			}
		}

		/// <summary>
		/// Initialize the local cache for the attached drive. <seealso cref="ICloudDriveManager.InitializeCache"/>
		/// </summary>
		/// <param name="cacheRootPath">
		/// The cache Root Path.
		/// </param>
		/// <param name="totalCacheInMegabytes">
		/// The total Cache In Megabytes.
		/// </param>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking.")]
		public void InitializeCache(DirectoryInfo cacheRootPath, int totalCacheInMegabytes)
		{
			Assumes.NotNull(cacheRootPath, "cacheRootPath");

			if (cacheInitialized)
			{
				trace.Information(Strings.LocalDriveCacheAlreadyInitialized);
				return;
			}

			trace.Information(Strings.LocalDriveCacheInitializing);

			CloudDrive.InitializeCache(cacheRootPath.FullName, totalCacheInMegabytes);
			TotalCacheInMegabytes = totalCacheInMegabytes;
			RemainingCache = totalCacheInMegabytes;

			trace.Information(Strings.LocalDriveCacheInitialized, cacheRootPath.FullName, totalCacheInMegabytes);
			cacheInitialized = true;
		}

		/// <summary>
		/// Create a cloud drive in blob storage that does not exist.
		/// </summary>
		/// <param name="storageCredentials">The Windows Azure Storage Credentials to access Cloud Storage.</param>
		/// <param name="driveAddress">The blob VHD dive.</param>
		/// <returns>True if the specific Cloud Drive (Page Blob) exists.</returns>
		/// <Exception cref="CloudDriveManagerException">If the blob exists at the location but is not a Page Blob.</Exception>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking."),
		 SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking.")]
		public bool Exists(IStorageCredentials storageCredentials, Uri driveAddress)
		{
			Assumes.NotNull(storageCredentials, "storageCredentials");
			Assumes.NotNull(driveAddress, "driveAddress");

			var cloudBlob = new CloudBlob(driveAddress.AbsoluteUri, storageCredentials.Unwrap());

			if (!cloudBlob.Container.Exists() || !cloudBlob.Exists())
			{
				return false;
			}

			if (cloudBlob.Attributes.Properties.BlobType != BlobType.PageBlob)
			{
				trace.Error(Strings.BlobInAccountDoesNotExist, driveAddress.AbsoluteUri, storageCredentials.AccountName);
				throw new CloudDriveManagerException(string.Format(CultureInfo.InvariantCulture, Strings.BlobInAccountDoesNotExist, driveAddress.AbsoluteUri, storageCredentials.AccountName));
			}

			return true;
		}

		/// <summary>
		/// Create a cloud drive in blob storage that does not exist.
		/// </summary>
		/// <param name="storageCredentials">The Windows Azure Storage Credentials to access Cloud Storage.</param>
		/// <param name="driveAddress">The blob VHD dive.</param>
		/// <param name="sizeInMegabytes">If the drive to mount does not exist then create it with the given size.</param>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking."), 
		 SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking.")]
		public void Create(IStorageCredentials storageCredentials, Uri driveAddress, int sizeInMegabytes)
		{
			Assumes.NotNull(storageCredentials, "storageCredentials");
			Assumes.NotNull(driveAddress, "driveAddress");

			if (Exists(storageCredentials, driveAddress))
			{
				if (!storageCredentials.AreDevelopmentStorageCredentials())
				{
					trace.Error(string.Format(CultureInfo.InvariantCulture, Strings.BlobExistsCannotCreate, storageCredentials.AccountName, driveAddress.AbsoluteUri));
					throw new CloudDriveManagerException(string.Format(CultureInfo.CurrentCulture, Strings.BlobExistsCannotCreate, storageCredentials.AccountName, driveAddress.AbsoluteUri));
				}
			}

			var cloudDrive = new CloudDrive(driveAddress, storageCredentials.Unwrap());

			if (sizeInMegabytes < CloudDriveSize.MinValue || sizeInMegabytes > CloudDriveSize.MaxValue)
			{
				// Data makes no sense.
				trace.Warning(Strings.CloudDriveSizeOutOfRange, sizeInMegabytes);
				return;
			}

			trace.Information(Strings.CloudDriveBlobCreating, cloudDrive.Uri.AbsoluteUri);

			try
			{
				cloudDrive.Create(sizeInMegabytes);
				trace.Information(Strings.CloudDriveBlobCreated, cloudDrive.Uri.AbsoluteUri);
			}
			catch (CloudDriveException cde)
			{
				// This is the compute emulator windows azure drive special case:
				// On local disk drives are not stored in blob storage and any cloudBlob.Exists() call will allways fail.
				// This means this exception is very likely OK. Just proceed as if nothing happened for DevelopmentStorageAccount.
				if (!storageCredentials.AreDevelopmentStorageCredentials())
				{
					var message = CreateAndTraceExceptionMessage(cde, string.Format(CultureInfo.CurrentCulture, Strings.CloudDriveBlobUnableToCreate, cloudDrive.Uri.AbsoluteUri));
					throw new CloudDriveManagerException(message, cde);
				}
			}
		}

		/// <summary>
		/// Mount a Windows Azure Drive and return the DirectoryInfo for it.
		/// </summary>
		/// <param name="storageCredentials">The Windows Azure Storage Credentials to access Cloud Storage.</param>
		/// <param name="driveAddress">The blob VHD dive.</param>
		/// <returns>A <see cref="DirectoryInfo"/> pointing to the resource.</returns>
		public DirectoryInfo Mount(IStorageCredentials storageCredentials, Uri driveAddress)
		{
			return Mount(storageCredentials, driveAddress, 0, DriveMountOptions.Force);
		}

		/// <summary>
		/// Mount a Windows Azure Drive and return the DirectoryInfo for it.
		/// </summary>
		/// <param name="storageCredentials">The Windows Azure Storage Credentials to access Cloud Storage.</param>
		/// <param name="driveAddress">The blob VHD dive.</param>
		/// <param name="cacheSize">The local cache size to use for this drive.</param>
		/// <returns>A <see cref="DirectoryInfo"/> pointing to the resource.</returns>
		public DirectoryInfo Mount(IStorageCredentials storageCredentials, Uri driveAddress, int cacheSize)
		{
			return Mount(storageCredentials, driveAddress, cacheSize, DriveMountOptions.Force);
		}

		/// <summary>
		/// Mount a Windows Azure Drive and return the DirectoryInfo for it.
		/// </summary>
		/// <param name="storageCredentials">The Windows Azure Storage Credentials to access Cloud Storage.</param>
		/// <param name="driveAddress">The blob VHD dive.</param>
		/// <param name="driveMountOptions">Drive mounting options.</param>
		/// <returns>A <see cref="DirectoryInfo"/> pointing to the resource.</returns>
		public DirectoryInfo Mount(IStorageCredentials storageCredentials, Uri driveAddress, DriveMountOptions driveMountOptions)
		{
			return Mount(storageCredentials, driveAddress, 0, driveMountOptions);
		}

		/// <summary>
		/// Mount a Windows Azure Drive and return the DirectoryInfo for it.
		/// </summary>
		/// <param name="storageCredentials">The Windows Azure Storage Credentials to access Cloud Storage.</param>
		/// <param name="driveAddress">The blob VHD dive.</param>
		/// <param name="cacheSize">The local cache size to use for this drive.</param>
		/// <param name="driveMountOptions">Drive mounting options.</param>
		/// <returns>A <see cref="DirectoryInfo"/> pointing to the resource.</returns>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking."),
		 SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking.")]
		public DirectoryInfo Mount(IStorageCredentials storageCredentials, Uri driveAddress, int cacheSize, DriveMountOptions driveMountOptions)
		{
			Assumes.NotNull(storageCredentials, "storageCredentials");
			Assumes.NotNull(driveAddress, "driveAddress");

			if (!Exists(storageCredentials, driveAddress))
			{
				if (!storageCredentials.AreDevelopmentStorageCredentials())
				{
					var message = string.Format(CultureInfo.CurrentCulture, Strings.CannotFindCloudDriveBlobAtAccountAndLocation, storageCredentials.AccountName, driveAddress.AbsoluteUri);
					trace.Error(message);
					throw new CloudDriveManagerException(message);
				}
			}

			if (cacheSize < 0)
			{
				trace.Warning(Strings.CloudDriveSizeOutOfRange);
				cacheSize = 0;
			}

			if (cacheSize > RemainingCache)
			{
				trace.Warning(Strings.CloudDriveCacheRequestTooBig, cacheSize, RemainingCache);
				cacheSize = RemainingCache;
			}

			WarmUpCache();

			var cloudDrive = new CloudDrive(driveAddress, storageCredentials.Unwrap());

			trace.Information(Strings.MountingDriveForBlobOnAccount, cloudDrive.Uri.AbsolutePath, cloudDrive.Credentials.AccountName);

			var microsoftDriveMountOptions = driveMountOptions.ToEnum<Microsoft.WindowsAzure.StorageClient.DriveMountOptions>();

			string driveRootPath;

			try
			{
				driveRootPath = cloudDrive.Mount(cacheSize, microsoftDriveMountOptions);
				RemainingCache = RemainingCache - cacheSize;

				trace.Information(Strings.DriveMountedForBlobToLocalPath, cloudDrive.Uri.AbsolutePath, driveRootPath);
			}
			catch (CloudDriveException cde)
			{
				var message = CreateAndTraceExceptionMessage(cde, string.Format(CultureInfo.CurrentCulture, Strings.CloudDriveBlobUnableToMount, cloudDrive.Uri.AbsoluteUri));
				throw new CloudDriveManagerException(message, cde);
			}

			managedDrives.Add(new DirectoryInfo(cloudDrive.LocalPath), new DriveAndCredentials { CloudDrive = cloudDrive, StorageCredentials = storageCredentials.Unwrap() });

			return new DirectoryInfo(driveRootPath);
		}

		/// <summary>
		/// Dismount the drive mounted to the rootDirectory.
		/// </summary>
		/// <param name="driveRootDirectory">The root directory for the mount.</param>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking.")]
		public void Dismount(DirectoryInfo driveRootDirectory)
		{
			Assumes.NotNull(driveRootDirectory, "driveRootDirectory");

			CloudDrive cloudDrive;

			if (!TryGetDrive(driveRootDirectory, out cloudDrive))
			{
				return;
			}

			lock (this)
			{
				WarmUpCache();

				if (IsMounted(cloudDrive))
				{
					trace.Information(Strings.DismountingCloudDriveBlobDetailed, cloudDrive.Credentials.AccountName, cloudDrive.Uri.AbsoluteUri, cloudDrive.LocalPath);

					cloudDrive.Unmount();
				}

				managedDrives.Remove(driveRootDirectory);

				trace.Information(Strings.DismountedCloudDriveBlobDetailed, cloudDrive.Credentials.AccountName, cloudDrive.Uri.AbsoluteUri, cloudDrive.LocalPath);
			}
		}

		/// <summary>
		/// Dismount and delete from blob storage the drive mounted to the rootDirectory.
		/// </summary>
		/// <param name="driveRootDirectory">The root directory for the mount.</param>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "AzureContrib.WindowsAzure.Core.Assumes.NotNull handles null checking."), 
		 SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "0", Justification = "This method checks for existance and creates if not exists. It is the right design for an extension method.")]
		public void Delete(DirectoryInfo driveRootDirectory)
		{
			Assumes.NotNull(driveRootDirectory, "driveRootDirectory");

			CloudDrive cloudDrive;

			if (!TryGetDrive(driveRootDirectory, out cloudDrive))
			{
				return;
			}

			lock (this)
			{
				WarmUpCache();

				if (IsMounted(cloudDrive))
				{
					trace.Information(Strings.DeletingManagedDriveAtDirectory, driveRootDirectory.FullName);
					cloudDrive.Delete();
				}
				else
				{
					trace.Information(Strings.ManagedDriveAtLocationNotMounted, driveRootDirectory.FullName);
				}

				managedDrives.Remove(driveRootDirectory);
			}
		}

		private static bool IsMounted(CloudDrive cloudDrive)
		{
			return CloudDrive.GetMountedDrives().Any(keyValuePair => keyValuePair.Key == cloudDrive.LocalPath);
		}

		private PageBlobDriveInfo CreateGetEnumeratorItem(KeyValuePair<string, Uri> item)
		{
			var rootDirectory = new DirectoryInfo(item.Key);
			var uri = item.Value;
		
			var storageCredentials = managedDrives.ContainsKey(rootDirectory) ? managedDrives[rootDirectory].StorageCredentials : null;

			return new PageBlobDriveInfo(rootDirectory, uri, storageCredentials.Wrap());
		}

		private bool TryGetDrive(DirectoryInfo driveRootDirectory, out CloudDrive cloudDrive)
		{
			DriveAndCredentials driveAndCredentials;
			if (managedDrives.TryGetValue(driveRootDirectory, out driveAndCredentials))
			{
				cloudDrive = driveAndCredentials.CloudDrive;
			}
			else
			{
				trace.Information(Strings.CloudDriveManagerNotManagingTheDrive, GetType().FullName, driveRootDirectory.FullName);
				cloudDrive = null;
			}

			return cloudDrive != null;
		}

		private void WarmUpCache()
		{
			if (cacheInitialized)
			{
				return;
			}

			trace.Information(Strings.FetchingLocalResourceForDrive, localResourceCacheName);

			ILocalResource localResource;
			try
			{
				localResource = localResourceManager.GetLocalResource(localResourceCacheName);
				trace.Information(Strings.FoundLocalResourceForPath, localResource.Name, localResource.RootPath, localResource.MaximumSizeInMegabytes);
			}
			catch (CloudDriveManagerException cdme)
			{
				var message = CreateAndTraceExceptionMessage(cdme, string.Format(CultureInfo.CurrentCulture, Strings.UnableToGetLocalResourceNamed, localResourceCacheName));
				throw new CloudDriveManagerException(message, cdme);
			}

			InitializeCache(localResource.RootPath, localResource.MaximumSizeInMegabytes);
		}

		private string CreateAndTraceExceptionMessage(Exception e, string baseMessage)
		{
			var message = new StringBuilder(baseMessage)
				.AppendFormat(Strings.ErrorMessage, e.Message)
				.AppendFormat(Strings.StackTrace, e.StackTrace);
			var exceptionMessage = message.ToString();
			trace.Error(exceptionMessage);
			return exceptionMessage;
		}

		private class DriveAndCredentials
		{
			public CloudDrive CloudDrive { get; set; }

			public StorageCredentials StorageCredentials { get; set; }
		}
	}
}